using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Text;

namespace InfiniTec.Security
{
	/// <summary>
	/// Provides the ability to translate a <see cref="NTAccount"/> to a <see cref="SecurityIdentifier"/>
	/// and vice versa, optionally using a remote computer for the translation process.
	/// </summary>
	public class IdentityResolver : IEquatable<IdentityReference>
	{
		private readonly string _ComputerName;
		private NTAccount _Account;
		private AccountType _AccountType;
		private SecurityIdentifier _Sid;

		/// <summary>
		/// Creates a new instance of this class. A remote computer is not used for the 
		/// translation process
		/// </summary>
		/// <param name="identity">The identity to translate</param>
		public IdentityResolver(IdentityReference identity) : this(identity, null) {}

		/// <summary>
		/// Creates a new instance of this class.
		/// </summary>
		/// <param name="identity">The identity to translate</param>
		/// <param name="computerName">The computer to use for the translation</param>
		/// <remarks>The remote computer is not used for translation, if
		/// the provided identity is a <see cref="SecurityIdentifier"/> and the domain
		/// sid of that identity equals the domain sid of the current user</remarks>
		public IdentityResolver(IdentityReference identity, string computerName)
		{
			_Sid = identity as SecurityIdentifier;
			_Account = identity as NTAccount;

			if (_Sid == null || !_Sid.IsEqualDomainSid(WindowsIdentity.GetCurrent().User))
			{
				_ComputerName = computerName;
			}
		}

		public bool TryTranslation()
		{
			if (_Sid != null && _Account == null)
			{
				NTAccount account;
				if (TryTranslateFromSecurityDescriptor(out account)) return true;
			}
			else if (_Sid == null && _Account == null)
			{
				SecurityIdentifier sid;
				if (TryTranslateFromNTAccount(out sid)) return true;
			}
			return false;
		}
		/// <summary>
		/// Returns the <see cref="SecurityIdentifier"/> of this instance.
		/// </summary>
		public SecurityIdentifier Sid
		{
			get
			{
				if (_Sid == null) TranslateFromNTAccount();
				return _Sid;
			}
		}

		/// <summary>
		/// Returns the <see cref="NTAccount"/> of this instance.
		/// </summary>
		public NTAccount Account
		{
			get
			{
				if (_Account == null) TranslateFromSecurityDescriptor();
				return _Account;
			}
		}

		/// <summary>
		/// Returns the <see cref="AccountType"/> of this instance
		/// </summary>
		public AccountType AccountType
		{
			get
			{
				if (_Sid == null) TranslateFromNTAccount();
				else if (_Account == null) TranslateFromSecurityDescriptor();

				return _AccountType;
			}
		}

		/// <summary>
		/// Compares this identity with the specified othe identity.
		/// </summary>
		/// <param name="other">The identity to compare this identity with.</param>
		/// <returns>True, if both identities represent the same identity</returns>
		public bool Equals(IdentityReference other)
		{
			if (other == null) return false;

			if (other is NTAccount) return Account.Equals(other);
			if (other is SecurityIdentifier) Sid.Equals((SecurityIdentifier) other);

			return false;
		}

		private void TranslateFromNTAccount()
		{
			SecurityIdentifier sid;
			bool result = TryTranslateFromNTAccount(out sid);

			CheckTranslationResult(result);
			
			_Sid = sid;
		}

		private bool TryTranslateFromNTAccount(out SecurityIdentifier sid)
		{
			var binarySid = new byte[SecurityIdentifier.MaxBinaryLength];
			var binarySidLength = (uint) binarySid.Length;
			var referencedDomain = new StringBuilder(0xff);
			var referencedDomainLength = (uint) referencedDomain.Capacity;

			bool result = NativeMethods.LookupAccountName(_ComputerName, _Account.Value,
			                                              binarySid, ref binarySidLength, referencedDomain,
			                                              ref referencedDomainLength, out _AccountType);

			sid = new SecurityIdentifier(binarySid, 0);
			return result;
		}

		private void CheckTranslationResult(bool result)
		{
			if (!result)
			{
				var error = (WindowsStatusCode) Marshal.GetLastWin32Error();
				if (error == WindowsStatusCode.NoMappingPerformed)
				{
					throw new IdentityNotMappedException(string.Format("Failed to retrieve the SID for user {0}. No mapping was performed by the operating system.", _Account.Value));
				}
				throw new Win32Exception((int) error);
			}
		}

		private void TranslateFromSecurityDescriptor()
		{
			NTAccount account;
			bool result = TryTranslateFromSecurityDescriptor(out account);

			CheckTranslationResult(result);

			_Account = account;
		}

		private bool TryTranslateFromSecurityDescriptor(out NTAccount account)
		{
			var name = new StringBuilder(0x100);
			var domain = new StringBuilder(0xff);

			uint nameLength = (uint) name.Capacity - 2;
			uint domainLength = (uint) domain.Capacity - 2;

			var binarySid = new byte[_Sid.BinaryLength];
			_Sid.GetBinaryForm(binarySid, 0);

			bool result = NativeMethods.LookupAccountSid(
				_ComputerName, binarySid, name, ref nameLength, domain, ref domainLength, out _AccountType);
			account = new NTAccount(domain.ToString(), name.ToString());
			return result;
		}
	}
}